package game.connection;

import game.Field;
import game.Food;
import game.GameConfiguration;
import game.Player;
import game.event.Referee;
import game.snake.Snake;
import game.snake.SnakeBody;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Observable;
import java.util.Observer;
import java.util.Random;

import com.lloseng.ocsf.server.AbstractServer;
import com.lloseng.ocsf.server.ConnectionToClient;

/**
 * Server of snake game. Manages the the event from all players and distributes
 * the result.
 * 
 * @author Sarun Wongtanakarn 5510546166
 * @author Apiwat Jierakulpanit 5510546212
 * @Version 2013.04.23
 */
public final class GameServer extends AbstractServer implements Observer {

	private Integer localID = 1;
	private HashMap<Integer, ConnectionToClient> playerClient;
	private HashMap<Integer, Player> player;
	private GameConfiguration gameConfiguration;
	private Announcer chatBoxAnnouncer;

	private boolean haveFood;
	private Field field;
	private Random random;
	private Food food;
	private ServerState state;

	/**
	 * Creates a new game server with game configuration.
	 * 
	 * @param port
	 *            server port.
	 * @param gameConfiguration
	 *            the configuration of this game.
	 */
	public GameServer(int port, GameConfiguration gameConfiguration) {
		super(port);
		this.gameConfiguration = gameConfiguration;
		field = gameConfiguration.FIELD;
		random = new Random();
		state = ServerState.WAITING;

		playerClient = new HashMap<Integer, ConnectionToClient>();
		player = new HashMap<Integer, Player>();
		ChatBox chatBox = new ChatBox("Server");
		chatBox.run();
		chatBox.addObserver(this);
		chatBoxAnnouncer = new Announcer();
		chatBoxAnnouncer.addObserver(chatBox);
	}

	@Override
	protected void clientConnected(ConnectionToClient client) {

		if (isServerFull(client))
			return;

		client.setInfo("localID", localID++);
		playerClient.put((Integer) client.getInfo("localID"), client);

		try {
			client.sendToClient(new Text("Hello! Player"
					+ client.getInfo("localID") + "."));
		} catch (IOException e) {
			e.printStackTrace();
		}

		print("[System] " + client.getInfo("localID") + " connected");

	}

	/**
	 * Checks the number of connected client equal to max player.
	 * 
	 * @param client
	 *            the new client that has connected
	 * @return true if the number of connected client equal to max player.
	 *         Otherwise return false.
	 */
	private boolean isServerFull(ConnectionToClient client) {

		if (playerClient.size() == gameConfiguration.MAX_PLAYER) {
			try {
				client.sendToClient(new Text("Sorry, Server is full."));
				return true;
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		return false;
	}

	@Override
	protected synchronized void clientDisconnected(ConnectionToClient client) {
		print("[System] " + client.getInfo("name") + " '"
				+ client.getInfo("localID") + "' disconnected");
	}

	@Override
	protected void handleMessageFromClient(Object arg, ConnectionToClient client) {
		if (!(arg instanceof Package)) {
			ejectClient(client);
			return;
		}

		Package pack = (Package) arg;

		if (pack.getType() == PackageType.TEXT) {
			handleText(client, (String) pack.unpack());
		}

		if (pack.getType() == PackageType.GAME_EVENT) {
			handleGameEvent(client, (GameEvent) pack.unpack());
		}

	}

	/**
	 * Receives the event from client.
	 * 
	 * @param client
	 *            the client that has sent event to server
	 * @param event
	 *            the event that has been sent
	 */
	private void handleGameEvent(ConnectionToClient client, GameEvent event) {
		Integer id = (Integer) client.getInfo("localID");
		updateGameEvent(id, event);
	}

	/**
	 * Receives the text from client.
	 * 
	 * @param client
	 *            the client that has sent text to server
	 * @param text
	 *            the text that has been sent
	 */
	private void handleText(ConnectionToClient client, String text) {

		if (client.getInfo("name") == (null)) {
			client.setInfo("name", text);
			print(("[System] usr'" + client.getId() + "' = " + text));
			playerClient.put((Integer) client.getInfo("localID"), client);
			checkStart();
		} else {
			sendToAllClients(new Text(client.getInfo("name") + ": " + text));
			print(client.getInfo("name") + ": " + text);
		}

	}

	/**
	 * Sends the refuse String to the client.
	 * 
	 * @param client
	 *            the client that connect to server
	 */
	private void ejectClient(ConnectionToClient client) {
		print(client.getId() + "sent an illegal message");
		try {
			client.sendToClient("You sent and illegal message!\n Connection will be terminated...");
			client.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	/** Checks for start the game. */
	private void checkStart() {

		if (playerClient.size() == gameConfiguration.MAX_PLAYER
				&& state == ServerState.WAITING) {

			preparePlayer();
			plotSnakeBody();
			checkForCreateFood();
			prepareGame();
			state = ServerState.RUNNING;
			print("[Game] Start a new game.");
		}

	}

	/** Checks for create the new food. */
	private void checkForCreateFood() {

		if (state == ServerState.RUNNING) {

			if (!haveFood) {
				food = new Food(
						field.POINT[random.nextInt(field.getWidth() - 2) + 1][random
								.nextInt(field.getHeight() - 2) + 1], 9);
				field.setValue(food.getPoint(), food.getScore());
				haveFood = true;
			}

			if (haveFood && field.getValue(food.getPoint()) != food.getScore()) {
				haveFood = false;
				checkForCreateFood();
			}
		}
	}

	/**
	 * Updates the game in server by event.
	 * 
	 * @param id
	 *            the id of client that has sent event
	 * @param incomingEvent
	 *            the event that has been sent
	 */
	protected void updateGameEvent(int id, GameEvent incomingEvent) {
		try {

			player.get(id).setHealth(
					incomingEvent.getOpponentData().get(id).intValue());

			Map<Integer, GameEvent> result = (new Referee()).handleEvent(
					incomingEvent, field, id, localID - 1, food);

			checkForCreateFood();

			for (int i = 1; i < localID; i++) {
				result.get(i).addMoveEvent(food.getPoint(), food.getScore());
				for (int j = 1; j < localID; j++) {

					result.get(i).addOpponentData(j, player.get(j).getHealth());

				}

				try {
					playerClient.get(i).sendToClient(result.get(i));
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		} catch (Exception e) {

		}
	}

	/** 
	 * Sends the text to all clients
	 * @param text the text that will be sent
	 */
	public void print(String text) {
		chatBoxAnnouncer.announce(text);
	}

	/** Creates the players. */
	public void preparePlayer() {
		for (int i = 1; i < localID; i++) {
			String name = (String) playerClient.get(i).getInfo("name");
			if (name == null)
				name = "Player" + i;

			Player p = new Player(i, name, new Snake(field.POINT[i * 2][i * 2],
					gameConfiguration.STARTING_LENGTH, field),
					gameConfiguration.STARTING_MONEY,
					gameConfiguration.STARTING_HEALTH,
					gameConfiguration.MAXIMUM_HEALTH);

			player.put(p.ID, p);
		}
	}

	/** Sets the value in the point on field up to SnakeBody. */
	private void plotSnakeBody() {
		for (int i = 1; i < localID; i++) {
			for (SnakeBody body : player.get(i).getSnake().getBody()) {
				field.setValue(body.getPoint(), player.get(i).ID);
			}
		}
	}

	/**
	 *  Sets the name of players.
	 * Creates GameInitiates and send each of it to all players.
	 */
	private void prepareGame() {
		Map<Integer, String> playerNames = new HashMap<Integer, String>();
		for (int i = 1; i < localID; i++) {
			String name = (String) playerClient.get(i).getInfo("name");
			if (name == null)
				name = "Player" + i;
			playerNames.put(i, name);
		}

		for (int i = 1; i < localID; i++) {
			GameInitiate game = new GameInitiate(field, player.get(i),
					gameConfiguration.MAX_PLAYER, gameConfiguration.INTERVAL,
					playerNames);
			try {
				playerClient.get(i).sendToClient(game);

			} catch (IOException e) {
				e.printStackTrace();
			}
		}

	}

	@Override
	public void update(Observable o, Object arg) {
		this.sendToAllClients(new Text(arg.toString()));
	}

}
